package uk.ac.lkl.lpp.ui;



import java.util.ArrayList;
import java.util.HashMap;

import java.awt.Color;
import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.JButton;

import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;

import uk.ac.lkl.common.ui.CellView;
import uk.ac.lkl.common.ui.View;
import uk.ac.lkl.common.ui.ViewCreator;
import uk.ac.lkl.common.util.Property; 
//import uk.ac.lkl.common.util.property.IntegerProperty;

import uk.ac.lkl.lpp.model.PedagogyPlanModel;
import uk.ac.lkl.lpp.model.TeachingMethodTableModel;


// hack

//import uk.ac.lkl.lpp.model.ldo.CognitiveActivity;

/**
 * A table that uses a properly-parented Component for each cell.
 * 
 * It is intended to be used as a substitute for a JTable, using components
 * directly (in a GridBagLayout) rather than parenting them just for the
 * duration of paint calls. In common with JTable, this class takes a TableModel
 * in its constructor.
 * 
 * <p>
 * <b>Note:</b> Currently, this class is specific to showing a table of
 * TeachingMethods. This needs to be factored out.
 * </p>
 * 
 * @author Darren Pearce
 * @version $Revision$
 * @version $Date$
 * 
 */
public class CTable extends JPanel {
	private static final long serialVersionUID = 238914202877452451L;


	/**
     * An immutable reference to a specific cell.
     * 
     * @author Darren Pearce
     * @version $Revision$
     * @version $Date$
     * 
     */
    public class GridReference {

        private int rowIndex;


        private int columnIndex;


        public GridReference(int rowIndex, int columnIndex) {
            this.rowIndex = rowIndex;
            this.columnIndex = columnIndex;
        }


        public int getRowIndex() {
            return rowIndex;
        }


        public int getColumnIndex() {
            return columnIndex;
        }
    };


    /**
     * The layout used by this class to render the table.
     * 
     * Currently this is always a GridBagLayout.
     * 
     */
    private GridBagLayout layout;


    public final static String TMs_TABLE_PREFIX = "TeachingMethodTable.";


    /**
     * The table model of this instance.
     * 
     * Note: this is a teaching method table since this has support for field
     * groups.
     * 
     */
    private TeachingMethodTableModel tableModel;


    /**
     * The grid of cells in this instance.
     * 
     * This is stored as an ArrayList of rows, each of which is represented in
     * turn by another ArrayList.
     * 
     */
    private ArrayList<ArrayList<View>> cells;


    private HashMap<GridReference, ViewCreator> cellCreatorMap;


    private HashMap<Integer, ViewCreator> columnCreatorMap;


    private GridBagConstraints c = new GridBagConstraints();


    private ArrayList<JLabel> columnTotals;


    // all components involved in calculation of totals. Includes labels. Is so
    // can shift around.
    private ArrayList<Component> totalComponents;


    private JLabel targetTotalField;


    //private Property creditHoursProperty;
    int creditHours;


    // hack
    private HashMap<CellView, GridReference> cellViewMap;


    /**
     * For now just initialise from model.
     * 
     * @param tableModel
     *            the table model to use
     * 
     */
    public CTable(TeachingMethodTableModel tableModel) {
        this.tableModel = tableModel;
        cells = new ArrayList<ArrayList<View>>();
        cellCreatorMap = new HashMap<GridReference, ViewCreator>();
        columnCreatorMap = new HashMap<Integer, ViewCreator>();
        columnTotals = new ArrayList<JLabel>();
        totalComponents = new ArrayList<Component>();
        cellViewMap = new HashMap<CellView, GridReference>();
        creditHours = tableModel.getPedagogyPlanModel().getCreditTime();
        initialiseLayout();
        addTableModelListener();
    }


    private void addTableModelListener() {
        tableModel.addTableModelListener(new TableModelListener() {

            public void tableChanged(TableModelEvent e) {
                processTableChanged(e);
            }
        });
    }


    private void processTableChanged(TableModelEvent e) {
        switch (e.getType()) {
        case TableModelEvent.UPDATE:
            updateTableCells();
            break;
        case TableModelEvent.INSERT:
            insertTableRows(e);
            break;
        case TableModelEvent.DELETE:
            deleteTableRows(e);
            break;
        default:
        }
    }


    /**
     * Update the table cells.
     * 
     * This method is called when the underlying table model fires a
     * TableModelEvent with type 'UPDATE'.
     * 
     * This happens when a cell is edited.
     * 
     */
    private void updateTableCells() {
        int rowCount = tableModel.getRowCount();
        int columnCount = tableModel.getColumnCount();
        for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) {
            for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) {
                Object value = tableModel.getValueAt(rowIndex, columnIndex);
                updateCellModel(rowIndex, columnIndex, value);
            }
        }
        updateTotalCells();
        checkTotals();
        rowCount = tableModel.getRowCount();
    }


    /**
     * Insert rows into the GridLayout.
     * 
     * @param e
     *            the TableModelEvent
     * 
     */
    private void insertTableRows(TableModelEvent e) {
        int firstRow = e.getFirstRow();
        int lastRow = e.getLastRow();

        for (int rowIndex = firstRow; rowIndex <= lastRow; rowIndex++) {
            ArrayList<View> components = initialiseRow(cells.size());
            cells.add(components);
        }

        int numRows = lastRow - firstRow + 1;

        for (Component totalComponent : totalComponents) {
            GridBagConstraints c = layout.getConstraints(totalComponent);
            c.gridy += numRows;
            layout.setConstraints(totalComponent, c);
        }

        updateTableCells();
        validate();
    }


    private void deleteTableRows(TableModelEvent e) {
        int firstRow = e.getFirstRow();
        int lastRow = e.getLastRow();

        for (int rowIndex = firstRow; rowIndex <= lastRow; rowIndex++) {
            ArrayList<View> rowViews = cells.get(cells.size() - 1);
            for (View rowView : rowViews)
                remove(rowView);
            cells.remove(cells.size() - 1);
        }

        int numRows = lastRow - firstRow + 1;

        for (Component totalComponent : totalComponents) {
            GridBagConstraints c = layout.getConstraints(totalComponent);
            c.gridy -= numRows;
            layout.setConstraints(totalComponent, c);
        }

        updateTableCells();
        validate();
    }


    private void updateTotalCells() {
        for (int columnIndex = 1; columnIndex < tableModel.getColumnCount(); columnIndex++) {
            JLabel label = columnTotals.get(columnIndex);
            if (label == null)
                continue;
            Object total = tableModel.getColumnTotal(columnIndex);
            label.setText(CellView.FORMAT.format((Float) total));
        }
    }


    private void initialiseLayout() {
        layout = new GridBagLayout();
        setLayout(layout);
        c.gridwidth = 1;
        c.gridheight = 1;
        c.fill = GridBagConstraints.HORIZONTAL;
        c.weightx = 1.0;
        c.weighty = 1.0;

        int columnCount = tableModel.getColumnCount();
        int rowCount = tableModel.getRowCount();

        initialiseHeaders();

        for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) {
            ArrayList<View> components = initialiseRow(rowIndex);
            cells.add(components);
        }

        addTotals();
    }


    private ArrayList<View> initialiseRow(int rowIndex) {
        ArrayList<View> components = new ArrayList<View>();
        for (int columnIndex = 0; columnIndex < tableModel.getColumnCount(); columnIndex++) {
            c.gridx = columnIndex;
            c.gridy = rowIndex + 1; // add 1 for header row. Eventually make
            // this a lot more robust.
            Object value = tableModel.getValueAt(rowIndex, columnIndex);
            View view = createView(value, rowIndex, columnIndex);
            components.add(view);
            add(view, c);
        }
        return components;
    }


    private void addTotals() {
        int rowCount = tableModel.getRowCount();
        c.gridy = rowCount + 1;
        c.gridx = 0;
        JLabel totalLabel = new JLabel("Total:");
        add(totalLabel, c);
        totalComponents.add(totalLabel);

        for (int columnIndex = 0; columnIndex < tableModel.getColumnCount(); columnIndex++) {
            c.gridx = columnIndex;
            Object total = tableModel.getColumnTotal(columnIndex);
            if (total == null) {
                columnTotals.add(null); // so totals is in column-index order
                continue;
            }
            JLabel label = new JLabel(total.toString(), SwingConstants.RIGHT);
            label.setOpaque(true);
            columnTotals.add(label);
            totalComponents.add(label);

            // hack so that array is in columnIndex order. First one never used.
            if (columnIndex != 0)
                add(label, c);
        }

        c.gridx = 0;
        c.gridy = rowCount + 2;
        JLabel targetTotalLabel = new JLabel("Target:");
        add(targetTotalLabel, c);
        totalComponents.add(targetTotalLabel);

        c.gridx = 1;
        targetTotalField = new JLabel("", SwingConstants.RIGHT);
        targetTotalField.setOpaque(true);
        add(targetTotalField, c);
        totalComponents.add(targetTotalField);
        updateTargetHours();
        addPropertyListener();
    }


    private void addPropertyListener() {
        // hack - specific to pedagogy plan
    	tableModel.getPedagogyPlanModel().addChangeListener(new ChangeListener() {
            public void stateChanged(ChangeEvent e) {
                updateTargetHours();
            }
        });
    }


    private void updateTargetHours() {
        targetTotalField.setText(String.valueOf(tableModel.getPedagogyPlanModel().getCreditTime()));
        checkTotals();
    }


    // highlight if appropriate
    // hack
    private void checkTotals() {
//        int targetHours = Integer.parseInt(creditHoursProperty.getValue());
        int targetHours = tableModel.getPedagogyPlanModel().getCreditTime();

        JLabel totalHoursLabel = columnTotals.get(1);

        float totalHours = Float.parseFloat(totalHoursLabel.getText());
        if ((float) targetHours == totalHours) {
            totalHoursLabel.setBackground(Color.green);
            targetTotalField.setBackground(Color.green);
        }
        else {
            targetTotalField.setBackground(Color.orange);
            totalHoursLabel.setBackground(Color.orange);
        }
    }


    /**
     * Create the table headers.
     * 
     * The headers are defined in the tableModel, and the actual strings
     * retrieved from Messages. The top-left corner cell is a trigger for
     * opening the TMsSelectionDialog
     * 
     */
    private void initialiseHeaders() {
        c.gridy = 0;
        c.fill = GridBagConstraints.BOTH;

        // do the corner header ("Teaching Method") which act as a trigger for
        // selecting TMs
        c.gridx = 0;
        JComponent component = Messages.getJButton(TMs_TABLE_PREFIX
                + tableModel.getColumnName(0));
        component.addMouseListener(new MouseAdapter() {

            public void mouseClicked(MouseEvent e) {
                if (e.getClickCount() > 1) {
                    TeachingMethodsSelectDialog.showDialog(tableModel
                            .getPedagogyPlanModel(), e.getComponent()
                            .getLocationOnScreen());
                }
            }
        });
        add(component, c);
        c.gridx++;
        
        // now the next one, for hours / minutes
        add(new TimeButton(tableModel.getPedagogyPlanModel()));
        c.gridx++;

        String s = null;
        for (; c.gridx < tableModel.getColumnCount(); c.gridx++) {
        	s = tableModel.getColumnName(c.gridx);
            component = Messages.getJButton(TMs_TABLE_PREFIX+s);
            component.setBorder(BorderFactory.createEtchedBorder());
            add(component, c);
        }
    }


    /**
     * Create a view of the given object for the given cell location.
     * 
     * If a view creator specific to the given cell location has been specified
     * using <code>setCellViewCreator()</code> then this creator is used. If
     * no such creator exists, this method backs off to the column creator set
     * using <code>setColumnViewCreator()</code>. If this is also unset then
     * this method finally backs off to a <code>CellView</code> instance which
     * is based on a <code>JTextField</code>.
     * 
     * @param <O>
     *            the type of the object
     * @param object
     *            the object to create a view for
     * @param rowIndex
     *            its row index
     * @param columnIndex
     *            its column index
     * 
     * @return the appropriate view
     * 
     */
    private <O> View<O> createView(O object, int rowIndex, int columnIndex) {
        ViewCreator<O> viewCreator;
        GridReference gridReference = new GridReference(rowIndex, columnIndex);

        viewCreator = cellCreatorMap.get(gridReference);
        if (viewCreator != null)
            return viewCreator.createView(object);

        viewCreator = columnCreatorMap.get(columnIndex);
        if (viewCreator != null)
            return viewCreator.createView(object);

        CellView<O> textFieldView = new CellView<O>(tableModel, rowIndex,
                columnIndex, object);
        /*
         * textFieldView.addChangeListener(new ChangeListener() {
         * 
         * public void stateChanged(ChangeEvent e) {
         * processCellViewStateChanged(e); } });
         */

        cellViewMap
                .put(textFieldView, new GridReference(rowIndex, columnIndex));

        return textFieldView;
    }


    //
    // private void processCellViewStateChanged(ChangeEvent e) {
    // CellView cellView = (CellView) e.getSource();
    // try {
    // GridReference gridReference = cellViewMap.get(cellView);
    // int rowIndex = gridReference.getRowIndex();
    // int columnIndex = gridReference.getColumnIndex();
    // String textValue = cellView.getValue();
    // Object value;
    //
    // if (textValue.length() == 0) {
    // value = null;
    // }
    // else {
    // // need to factor this out properly
    // Class columnClass = tableModel.getColumnClass(columnIndex);
    // if (columnClass == Float.class) {
    // value = Float.parseFloat(textValue);
    // }
    // else if (columnClass == Integer.class) {
    // value = Integer.parseInt(textValue);
    // }
    // else
    // value = textValue;
    // }
    //
    // tableModel.setValueAt(value, rowIndex, columnIndex);
    // cellView.setValid(true);
    // cellView.updateView();
    //
    // }
    // catch (NumberFormatException nfe) {
    // cellView.setValid(false);
    // }
    // updateTableCells();
    // }

    /**
     * Set the view creartor for the specified column.
     * 
     * @param columnIndex
     *            the index of the column
     * @param viewCreator
     *            the view creator to use
     * 
     */
    public <O> void setColumnViewCreator(int columnIndex,
                                         ViewCreator<O> viewCreator) {
        columnCreatorMap.put(columnIndex, viewCreator);

        for (int rowIndex = 0; rowIndex < tableModel.getRowCount(); rowIndex++) {
            c.gridx = columnIndex;
            c.gridy = rowIndex + 1;
            Object value = tableModel.getValueAt(rowIndex, columnIndex);
            View view = createView(value, rowIndex, columnIndex);
            Component oldView = cells.get(rowIndex).get(columnIndex);
            remove(oldView);
            cells.get(rowIndex).set(columnIndex, view);
            add(view, c);
        }
    }


    /**
     * Set the model for a particular cell.
     * 
     * @param <O>
     *            the type of the model
     * @param rowIndex
     *            the row index
     * @param columnIndex
     *            the column index
     * @param model
     *            the model to set the cell to
     * 
     */
    private <O> void updateCellModel(int rowIndex, int columnIndex, O model) {
        c.gridx = columnIndex;
        c.gridy = rowIndex;
        View view = cells.get(rowIndex).get(columnIndex);
        view.setModel(model);
    }


    /**
     * Clear any existing view creator for the given column.
     * 
     * @param columnIndex
     *            the column index
     * 
     */
    public void clearColumnViewCreator(int columnIndex) {
        setColumnViewCreator(columnIndex, null);
    }


    /**
     * Set the view creator for a specific cell.
     * 
     * @param <O>
     *            the type that the view creator creates
     * @param rowIndex
     *            the row index
     * @param columnIndex
     *            the column index
     * @param viewCreator
     *            the view creator
     * 
     */
    public <O> void setCellViewCreator(int rowIndex,
                                       int columnIndex,
                                       ViewCreator<O> viewCreator) {
        cellCreatorMap.put(
                           new GridReference(rowIndex, columnIndex),
                           viewCreator);
    }


    /**
     * Clear any existing view creator for the given cell.
     * 
     * @param rowIndex
     *            the row index
     * @param columnIndex
     *            the column index
     * 
     */
    public void clearCellViewCreator(int rowIndex, int columnIndex) {
        setCellViewCreator(rowIndex, columnIndex, null);
    }


    /**
     * Get the component at the given row and column indices.
     * 
     * @param rowIndex
     *            the row index
     * @param columnIndex
     *            the column index
     * 
     * @return the relevant component
     * 
     */
    public Component getComponent(int rowIndex, int columnIndex) {
        return cells.get(rowIndex).get(columnIndex);
    }


    /**
     * Set whether the specified column is visible or not.
     * 
     * @param columnIndex
     *            the index of the column
     * @param visible
     *            its new visibility setting
     * 
     */
    public void setColumnVisible(int columnIndex, boolean visible) {
        for (int rowIndex = 0; rowIndex < tableModel.getRowCount(); rowIndex++)
            getComponent(rowIndex, columnIndex).setVisible(visible);
    }


    /**
     * Return whether the specified column is visible or not.
     * 
     * @param columnIndex
     *            the index of the column
     * 
     * @return <code>true</code> if the column is visible; <code>false</code>
     *         otherwise
     * 
     */
    public boolean isColumnVisible(int columnIndex) {
        for (int rowIndex = 0; rowIndex < tableModel.getRowCount(); rowIndex++)
            if (!getComponent(rowIndex, columnIndex).isVisible())
                return false;
        return true;
    }


    /**
     * Set whether the given row is visible or not.
     * 
     * @param rowIndex
     *            the index of the row
     * @param visible
     *            its new visibility setting
     * 
     */
    public void setRowVisible(int rowIndex, boolean visible) {
        for (int columnIndex = 0; columnIndex < tableModel.getColumnCount(); columnIndex++)
            getComponent(rowIndex, columnIndex).setVisible(visible);
    }


    /**
     * Return whether the specified row is visible or not.
     * 
     * @param rowIndex
     *            the index of the row
     * 
     * @return <code>true</code> if the row is visible; <code>false</code>
     *         otherwise
     * 
     */
    public boolean isRowVisible(int rowIndex) {
        for (int columnIndex = 0; columnIndex < tableModel.getColumnCount(); columnIndex++)
            if (!getComponent(rowIndex, columnIndex).isVisible())
                return false;
        return true;
    }

    private final class TimeButton extends JButton implements ChangeListener {
    	private PedagogyPlanModel model;
    	
    	public TimeButton (PedagogyPlanModel model) {
    		this.model = model;
    		setBorderPainted(false);
    		updateText();
    		model.addChangeListener(this);
    	}
    	
    	private void updateText() {
			String t = Messages.getLabel(model.getTime_unit());
			if (t==null || t.length() == 0) t =model.getTime_unit();
			setText(t);    		
    	}
    	
		public void stateChanged(ChangeEvent e){
			updateText();
		}    	
    }
}
